Using JSON Web Tokens (JWTs)
To authenticate to Omnicore, each device must prepare a JSON Web Token (JWT, RFC 7519). JWTs are used for short-lived authentication between devices and the MQTT or HTTP bridges. This page describes the Omnicore requirements for the contents of the JWT.
Omnicore does not require a specific token generation method. A good collection of helper client libraries can be found on JWT.io.
When creating an MQTT client, the JWT must be passed in the password field of the CONNECT message. When connecting over HTTP, a JWT must be included as bearer token in the 'Authorization' header of each HTTP request.
Mqtt Connection Field Key | Mqtt Connection Field Value |
---|---|
Client ID | subscriptions/{subscription-id}/registries/{registry-id}/devices/{device-id} |
Host | hostprefix.mqtt.korewireless.com |
Port | 8883 |
Username | unused |
Password | JWT Token |
SSL/TLS | True |
Certificate | Available |
SSL Secure | On |
Creating JWTs
JWTs are composed of three sections: a header, a payload (containing a claim set), and a signature. The header and payload are JSON objects, which are serialized to UTF-8 bytes, then encoded using base64url encoding.
The JWT's header, payload, and signature are concatenated with periods (.). As a result, a JWT typically takes the following form:
{Base64url encoded header}.{Base64url encoded payload}.{Base64url encoded signature}
The following sample illustrates how to create a Omnicore JWT . After creating the JWT, you can connect to the MQTT or HTTP bridge to publish messages from a device.
- c++
- Java
- Go
- NodeJs
- Python
/**
* Calculates issued at / expiration times for JWT and places the time, as a
* Unix timestamp, in the strings passed to the function. The time_size
* parameter specifies the length of the string allocated for both iat and exp.
*/
static void GetIatExp(char* iat, char* exp, int time_size) {
// TODO(#72): Use time.google.com for iat
time_t now_seconds = time(NULL);
snprintf(iat, time_size, "%lu", now_seconds);
snprintf(exp, time_size, "%lu", now_seconds + 3600);
if (TRACE) {
printf("IAT: %s\n", iat);
printf("EXP: %s\n", exp);
}
}
static int GetAlgorithmFromString(const char* algorithm) {
if (strcmp(algorithm, "RS256") == 0) {
return JWT_ALG_RS256;
}
if (strcmp(algorithm, "ES256") == 0) {
return JWT_ALG_ES256;
}
return -1;
}
/**
* Calculates a JSON Web Token (JWT) given the path to a EC private key .
* Returns the JWT as a string that the caller must
* free.
*/
static char* CreateJwt(const char* ec_private_path,
const char* algorithm) {
char iat_time[sizeof(time_t) * 3 + 2];
char exp_time[sizeof(time_t) * 3 + 2];
uint8_t* key = NULL; // Stores the Base64 encoded certificate
size_t key_len = 0;
jwt_t* jwt = NULL;
int ret = 0;
char* out = NULL;
// Read private key from file
FILE* fp = fopen(ec_private_path, "r");
if (fp == NULL) {
printf("Could not open file: %s\n", ec_private_path);
return "";
}
fseek(fp, 0L, SEEK_END);
key_len = ftell(fp);
fseek(fp, 0L, SEEK_SET);
key = malloc(sizeof(uint8_t) * (key_len + 1)); // certificate length + \0
fread(key, 1, key_len, fp);
key[key_len] = '\0';
fclose(fp);
// Get JWT parts
GetIatExp(iat_time, exp_time, sizeof(iat_time));
jwt_new(&jwt);
// Write JWT
ret = jwt_add_grant(jwt, "iat", iat_time);
if (ret) {
printf("Error setting issue timestamp: %d\n", ret);
}
ret = jwt_add_grant(jwt, "exp", exp_time);
if (ret) {
printf("Error setting expiration: %d\n", ret);
}
ret = jwt_set_alg(jwt, GetAlgorithmFromString(algorithm), key, key_len);
if (ret) {
printf("Error during set alg: %d\n", ret);
}
out = jwt_encode_str(jwt);
if (!out) {
perror("Error during token creation:");
}
// Print JWT
if (TRACE) {
printf("JWT: [%s]\n", out);
}
jwt_free(jwt);
free(key);
return out;
}
static MqttCallback mCallback;
static long MINUTES_PER_HOUR = 60;
/** Create a Omnicore JWT signed with the given RSA key. */
private static String createJwtRsa( String privateKeyFile)
throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
DateTime now = new DateTime();
// Create a JWT to authenticate this device. The device will be disconnected after the token
// expires, and will have to reconnect with a new token.
JwtBuilder jwtBuilder =
Jwts.builder()
.setIssuedAt(now.toDate())
.setExpiration(now.plusMinutes(20).toDate())
byte[] keyBytes = Files.readAllBytes(Paths.get(privateKeyFile));
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return jwtBuilder.signWith(SignatureAlgorithm.RS256, kf.generatePrivate(spec)).compact();
}
import (
"errors"
"io/ioutil"
"time"
jwt "github.com/golang-jwt/jwt"
)
// createJWT creates a Omnicore JWT for the given subscription id.
// algorithm can be one of ["RS256", "ES256"].
func createJWT( privateKeyPath string, algorithm string, expiration time.Duration) (string, error) {
claims := jwt.StandardClaims{
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(expiration).Unix(),
}
keyBytes, err := ioutil.ReadFile(privateKeyPath)
if err != nil {
return "", err
}
token := jwt.NewWithClaims(jwt.GetSigningMethod(algorithm), claims)
switch algorithm {
case "RS256":
privKey, _ := jwt.ParseRSAPrivateKeyFromPEM(keyBytes)
return token.SignedString(privKey)
case "ES256":
privKey, _ := jwt.ParseECPrivateKeyFromPEM(keyBytes)
return token.SignedString(privKey)
}
return "", errors.New("Cannot find JWT algorithm. Specify 'ES256' or 'RS256'")
}
const createJwt = ( privateKeyFile, algorithm) => {
// Create a JWT to authenticate this device. The device will be disconnected
// after the token expires, and will have to reconnect with a new token. The
const token = {
iat: parseInt(Date.now() / 1000),
exp: parseInt(Date.now() / 1000) + 20 * 60, // 20 minutes
};
const privateKey = readFileSync(privateKeyFile);
return jwt.sign(token, privateKey, {algorithm: algorithm});
};
def create_jwt( private_key_file, algorithm):
"""Creates a JWT (https://jwt.io) to establish an MQTT connection.
Args:
private_key_file: A path to a file containing either an RSA256 or
ES256 private key.
algorithm: The encryption algorithm to use. Either 'RS256' or 'ES256'
Returns:
A JWT generated from the given private key, which
expires in 20 minutes. After 20 minutes, your client will be
disconnected, and a new JWT will have to be generated.
Raises:
ValueError: If the private_key_file does not contain a known key.
"""
token = {
# The time that the token was issued at
"iat": datetime.datetime.now(tz=datetime.timezone.utc),
# The time the token expires.
"exp": datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(minutes=20),
}
# Read the private key file.
with open(private_key_file, "r") as f:
private_key = f.read()
print(
"Creating JWT using {} from private key file {}".format(
algorithm, private_key_file
)
)
return jwt.encode(token, private_key, algorithm=algorithm)
JWT header
The JWT header consists of two fields that indicate the signing algorithm and the type of token. Both fields are mandatory, and each field has only one value. Omnicore supports the following signing algorithms:
- JWT RS256 (RSASSA-PKCS1-v1_5 using SHA-256 RFC 7518 sec 3.3). This is expressed as RS256 in the alg field in the JWT header.
- JWT ES256 (ECDSA using P-256 and SHA-256 RFC 7518 sec 3.4), defined in OpenSSL as the prime256v1 curve. This is expressed as ES256 in the alg field in the JWT header.
In addition to the signing algorithm, you must supply the JWT token format.
The JSON representation of the header is as follows:
For RSA keys:
{ "alg": "RS256", "typ": "JWT" }
For Elliptic Curve keys:
{ "alg": "ES256", "typ": "JWT" }
The algorithm specified in the header must match at least one of the public keys registered for the device.
The JWT header is not the same as the HTTP header (if you're connecting over HTTP).
JWT claims
The JWT payload contains a set of claims, and it is signed using the asymmetric keys. The JWT claim set contains information about the JWT, such as the target of the token, the issuer, the time the token was issued, and/or the lifetime of the token. Like the JWT header, the JWT claim set is a JSON object and is used in the calculation of the signature.
Required claims
Omnicore requires the following reserved claim fields. They may appear in any order in the claim set.
Name | Description |
---|---|
iat | ("Issued At"): The timestamp when the token was created, specified as seconds since 00:00:00 UTC, January 1, 1970. The server may report an error if this timestamp is too far in the past or the future (allowing 10 minutes for skew). |
exp | ("Expiration"): The timestamp when the token stops being valid, specified as seconds since 00:00:00 UTC, January 1, 1970. The maximum lifetime of a token is 24 hours + skew. All MQTT connections will be closed by the server a few seconds after the token expires (allowing for skew), because MQTT does not have a way to refresh credentials. A new token must be minted to reconnect. Note that because of the allowed skew, in practice the minimum lifetime of a token will be be equal to the acceptable clock skew, even if it is set to one second. When connecting over HTTP, each HTTP request must include a JWT, regardless of expiration time. |
The nbf("Not Before") claim will be ignored, and is not required.
A JSON representation of the required reserved fields in a Omnicore JWT claim set is shown below:
For Elliptic Curve keys:
{
"iat": 1509654401,
"exp": 1612893233
}
JWT signature
The JSON Web Signature (JWS) specification guides the mechanics of generating the signature for the JWT. The input for the signature is the byte array of the following content:
{Base64url encoded header}.{Base64url encoded claim set}
To compute the signature, sign the base64url-encoded header, base64-url encoded claim set, and a secret key (such as an rsa_private.pem file) using the algorithm you defined in the header. The signature is then base64url-encoded, and the result is the JWT. The following example shows a JWT before base64url encoding:
{"alg": "RS256", "typ": "JWT"}.{ "iat": 1509654401, "exp": 1612893233}.[signature bytes]
After the final encoding, the JWT looks like the following:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjMxMDcwMjE5MDcsImlhdCI6MTY3MzU5Nzk0NH0.DNPWPXg8whGF66gycOJwnGcGv4bywtSt7GZEgeHrdrG_qJfIBNaYeeM1ElCy9bz9zw5X6qrXg-xdUsPTMgHCID3kiaZOu7yzdg4KXWIWQGAeWPUmeNXuXopvEvPu-398VDBuqXINTgf9O3WUBdzxHCW2iVOIJKvq7xybMZhcJmt_LEqlwGAM-xwE2-MSrnhnseLRkpIL_PH3YcHkfeb-0961XROFr-f5y3WEy8cyObt67iB_bO_QgShf0HQZwD6GFq-00D_HN7wdGYF4ruokV0SGLl-I7TkSqGdVbtLmDx38vXtF_S3ANegVNsu4pusvIHzXAcQ6MjOCuoYKNi8WjA